<script lang="ts">
    import { onMount } from 'svelte'
    import { STLLoader } from 'three/examples/jsm/loaders/STLLoader'
    import { TrackballControls } from 'three/examples/jsm/controls/TrackballControls'
    import { MeshPhongMaterial, Mesh, Sphere, Group, Scene, Box3,
        SpotLight, AmbientLight, Plane, Vector3,
        DoubleSide, FrontSide, BackSide,
        MeshStandardMaterial, MeshBasicMaterial,
        NotEqualStencilFunc, ReplaceStencilOp,
        PlaneGeometry, AlwaysStencilFunc,
        IncrementWrapStencilOp, DecrementWrapStencilOp
    } from 'three'

    import { createRenderer } from './utils/renderer'
    import { createPerspectiveCamera } from './utils/camera'

    import type { WebGLRenderer } from 'three'
    import type { BufferGeometry } from 'three'

    const stlLoader = new STLLoader()
    const url = '/models/stl2.stl'
    /* const url = '/models/fei.stl' */
    /* const url = '/models/mobius_ring.stl' */
    /* const url = '/models/教室.stl' */
    /* const url = '/models/涡轮发动机/涡轮发动机/part_11.stl' */
    const material = new MeshPhongMaterial({
        color: 0xFFC107,
        clipShadows: true,
        side: DoubleSide,
        shadowSide: DoubleSide
    })

    const pMat = new MeshStandardMaterial({
        color: 0x00ffff,
        opacity: 0.3,
        transparent: true
    })

    let width = 800
    let height = 600
    let aspect = width / height
    let canvas: HTMLCanvasElement
    let bbox = new Box3()
    let group = new Group()
    window.group = group
    let bsphere = new Sphere()
    let controls: TrackballControls
    let mesh: Mesh
    let scale: number = 71

    const camera = createPerspectiveCamera({ fov: 20, aspect, near: 1, far: 10000, zoom: 1 })
    let renderer: WebGLRenderer
    const scene = new Scene()
    window.scene = scene
    scene.add(group)
    const ambientLight = new AmbientLight( 0x888888, 0.6 )
    const spotLight = new SpotLight(0xffffff)

    function updateBBox () {
        bbox.makeEmpty()
        bbox.expandByObject( group )
        console.log(bbox)
        bbox.getBoundingSphere( bsphere )
        console.log(JSON.stringify(bsphere, null, 2))
    }

    function updateClippingPlane (mesh) {
        const geometry = mesh.geometry
        const constant = bsphere.center.z + bsphere.radius * (scale - 50) / 100
        const plane = new Plane(new Vector3(0, 0, -1), constant)

        if (renderer) {
            material.clippingPlanes = [plane]
        }
        const planeGeometry = new PlaneGeometry(bsphere.radius * 10, bsphere.radius * 10)
        const planeMaterial = new MeshStandardMaterial({
            color: 0x09fEf3,
            metalness: 0.1,
            roughness: 0.75,
            stencilWrite: true,
            depthWrite: true,
            stencilRef: 0,
            stencilFunc: NotEqualStencilFunc,
            stencilFail: ReplaceStencilOp,
            stencilZFail: ReplaceStencilOp,
            stencilZPass: ReplaceStencilOp
        })
        const planeMesh = new Mesh(planeGeometry, planeMaterial)
        planeMesh.onAfterRender = function (renderer) {
            renderer.clearStencil()
        }
        planeMesh.renderOrder = 2.1
        planeMesh.position.copy(bsphere.center)
        scene.add(planeMesh)

        const stencilGroup = createPlaneStencilGroup(geometry, plane, 2)
        group.add(stencilGroup)

        return function (scale: number) {
            plane.constant = bsphere.center.z + bsphere.radius * (scale - 50) / 100
        }
    }

    function updateCamera () {
        const { radius, center } = bsphere

        camera.position.copy( center.clone().addScalar( radius * 3.4 ) )
        camera.near = radius / 1000
        camera.far = radius * 50
        camera.up.set( 0, 1, 0 )
        camera.lookAt(center)
        camera.updateProjectionMatrix()
    }

    function initLights () {
        spotLight.position.set( 0, 1000, 0 )
        spotLight.angle = Math.PI * 2 / 3
        spotLight.intensity = 0.4

        scene.add(ambientLight)
        scene.add(spotLight)
    }

    function updateLights () {
        const { radius, center } = bsphere
        const dir = camera.position.clone().sub( center ).normalize()
        spotLight.position.copy( center.clone().addScaledVector( dir, radius * 100 ) )
    }

    function animate () {
        renderer.render(scene, camera)
        controls.update()
        requestAnimationFrame(() => {
            animate()
        })
    }

    function createPlaneStencilGroup (geometry: BufferGeometry,
                                      plane: Plane,
                                      renderOrder: number): Group {
        const group = new Group()
        const baseMat = new MeshBasicMaterial({
            depthWrite: false,
            depthTest: false,
            colorWrite: false,
            stencilWrite: true,
            stencilFunc: AlwaysStencilFunc
        })

        // back face
        const mat0 = baseMat.clone()
        mat0.side = BackSide
        mat0.clippingPlanes = [plane]
        /* mat0.stencilFail = IncrementWrapStencilOp */
        /* mat0.stencilZFail = IncrementWrapStencilOp */
        mat0.stencilZPass = IncrementWrapStencilOp

        const mesh0 = new Mesh(geometry, mat0)
        mesh0.renderOrder = renderOrder
        group.add(mesh0)

        // front face
        const mat1 = baseMat.clone()
        mat1.side = FrontSide
        mat1.clippingPlanes = [plane]
        /* mat1.stencilFail = DecrementWrapStencilOp */
        /* mat1.stencilZFail = DecrementWrapStencilOp */
        mat1.stencilZPass = DecrementWrapStencilOp

        const mesh1 = new Mesh(geometry, mat1)
        mesh1.renderOrder = renderOrder
        group.add(mesh1)

        return group
    }

    let clipper: Object
    onMount(() => {
        controls = new TrackballControls(camera, canvas)
        controls.zoomSpeed = 3
        controls.panSpeed = 1.7
        controls.rotateSpeed = 4
        controls.staticMoving = true
        initLights()
        renderer = createRenderer({ canvas, width: 800, height: 600 })
        window.renderer = renderer
        animate()
        stlLoader.load(url, (geo: BufferGeometry) => {
            mesh = new Mesh(geo, material)
            mesh.visible = true
            mesh.renderOrder = 1
            mesh.onAfterRender = function (renderer) {
                renderer.clearDepth()
            }
            group.add(mesh)
            updateBBox()
            clipper = updateClippingPlane(mesh)
            controls.target.copy(bsphere.center)
            updateCamera()
            updateLights()
        })
    })

    function handleRange (ev) {
        if (clipper) {
            clipper(scale)
        }
    }

</script>

<main>
    <h1>stl stencil</h1>
    <canvas id="hello" bind:this={canvas}></canvas>
    <div>
        <input name="volume" type="range"
            bind:value={scale}
            min="0" max="100" on:input={handleRange}>
        <span>{scale}</span>
    </div>
</main>

<style>
    #hello {
        width: 800px;
        height: 600px;
        border: 1px solid orange;
        background: #000ff0;
    }
    input[name="volume"] {
        width: 400px;
    }
</style>
